ATOM Documentation

← Back to App

Media Integration Documentation

Overview

The Atom SaaS platform provides integrated media control capabilities for Spotify and Apple Music, enabling agents and users to control playback, manage playlists, and discover music through a unified API.

Features

  • **Playback Control**: Play, pause, skip, seek, volume control, device transfer
  • **Voice Commands**: Natural language processing for media control
  • **Playlist Management**: Create, read, update, delete playlists across providers
  • **Music Discovery**: Recommendation engine with seed-based, genre-based, and mood-based strategies
  • **Multi-Provider Support**: Unified API for Spotify and Apple Music with automatic provider detection
  • **Agent Integration**: Agents can control media and manage playlists as part of workflows

Architecture

┌─────────────────┐
│   Frontend UI   │
│   (Next.js)     │
└────────┬────────┘
         │
         ▼
┌─────────────────────────────────┐
│   Media Service Layer           │
│   - PlaybackService             │
│   - PlaylistService             │
│   - RecommendationService       │
└────────┬────────────────────────┘
         │
         ▼
┌─────────────────────────────────┐
│   Integration Clients           │
│   - SpotifyClient               │
│   - AppleMusicClient            │
└────────┬────────────────────────┘
         │
         ▼
┌─────────────────────────────────┐
│   External APIs                 │
│   - Spotify Web API             │
│   - Apple Music API             │
└─────────────────────────────────┘

OAuth Setup

Spotify

  1. **Create Spotify App**
  • Go to https://developer.spotify.com/dashboard
  • Click "Create App"
  • Set app name and description
  1. **Configure Redirect URI**
  • Edit Settings → Redirect URIs
  • Add: https://your-domain.com/api/integrations/spotify/callback
  1. **Enable Required Scopes**
  • user-read-playback-state - Read playback state
  • user-modify-playback-state - Control playback
  • user-read-currently-playing - Read current track
  • user-read-email - Read user email
  • playlist-read-private - Read private playlists
  • playlist-modify-public - Modify public playlists
  • playlist-modify-private - Modify private playlists
  • streaming - Stream music
  1. **Set Environment Variables**

Apple Music

  1. **Create MusicKit Key**
  • Go to https://developer.apple.com/account/resources
  • Certificates, Identifiers & Profiles → Keys → Create Key
  • Configure MusicKit
  • Download .p8 file (can only download once!)
  1. **Generate JWT Developer Token**
  • The backend automatically generates JWT tokens using the private key
  • Tokens are valid for 6 months
  • Tokens are regenerated automatically when expired
  1. **Set Environment Variables**

**Important**: Use escaped newlines (\n) in the private key for environment variables.

API Reference

Playback Control

Get Current Playback State

GET /api/media/playback?provider=spotify

**Response:**

{
  "success": true,
  "data": {
    "state": {
      "isPlaying": true,
      "currentTrack": {
        "name": "Blinding Lights",
        "artist": "The Weeknd",
        "album": "After Hours",
        "uri": "spotify:track:0VjIjW4GlUZAMYd2vXMi3b",
        "artworkUrl": "https://i.scdn.co/image/...",
        "durationMs": 200000
      },
      "progressMs": 45000,
      "durationMs": 200000,
      "volumePercent": 75,
      "deviceId": "device-123",
      "provider": "spotify"
    }
  }
}

Execute Playback Action

POST /api/media/playback
Content-Type: application/json

{
  "action": "play",
  "value": "spotify:playlist:37i9dQZF1DX4sWSpwq3LiO",
  "provider": "spotify"
}

**Actions:**

  • play - Start playback (optional value: track/playlist URI)
  • pause - Pause playback
  • next - Skip to next track
  • previous - Go to previous track
  • volume - Set volume (value: 0-100)
  • seek - Seek to position (value: milliseconds)
  • transfer - Transfer playback to device (value: device ID)
  • search - Search for tracks (value: search query)
  • getDevices - List available devices

**Response:**

{
  "success": true,
  "data": {
    "executed": true,
    "state": { /* PlaybackState */ }
  }
}

Voice Commands

POST /api/media/voice
Content-Type: application/json

{
  "command": "play some jazz music",
  "provider": "spotify"
}

**Response:**

{
  "success": true,
  "data": {
    "parsed": {
      "original": "play some jazz music",
      "action": "play",
      "value": "jazz music",
      "confidence": 0.85,
      "provider": "spotify"
    },
    "state": { /* PlaybackState */ }
  }
}

**Supported Patterns:**

  • play <track/artist/genre> - Play music
  • pause / stop - Pause playback
  • skip / next - Next track
  • previous / back - Previous track
  • volume <0-100>% - Set volume
  • turn it up / turn it down - Adjust volume
  • play <track> by <artist> - Play specific track
  • search for <query> - Search music

Playlists

List Playlists

GET /api/media/playlists?provider=spotify

**Response:**

{
  "success": true,
  "data": {
    "playlists": [
      {
        "id": "playlist-123",
        "name": "My Favorites",
        "provider": "spotify",
        "trackCount": 50,
        "isPublic": false,
        "artworkUrl": "https://example.com/art.jpg"
      }
    ]
  }
}

Create Playlist

POST /api/media/playlists
Content-Type: application/json

{
  "name": "Road Trip Mix",
  "description": "Best driving songs",
  "isPublic": false,
  "provider": "spotify",
  "trackUris": ["spotify:track:1", "spotify:track:2"]
}

Add Tracks to Playlist

PUT /api/media/playlists
Content-Type: application/json

{
  "action": "add_tracks",
  "playlistId": "playlist-123",
  "trackUris": ["spotify:track:3", "spotify:track:4"]
}

Remove Tracks from Playlist

PUT /api/media/playlists
Content-Type: application/json

{
  "action": "remove_tracks",
  "playlistId": "playlist-123",
  "trackUris": ["spotify:track:3"]
}

Get Playlist Tracks

GET /api/media/playlists/playlist-123/tracks

Delete Playlist

DELETE /api/media/playlists?playlistId=playlist-123

Recommendations

Generate Recommendations

POST /api/media/recommendations
Content-Type: application/json

{
  "seedTracks": ["spotify:track:1", "spotify:track:2"],
  "seedArtists": ["spotify:artist:123"],
  "limit": 10,
  "provider": "spotify"
}

**Response:**

{
  "success": true,
  "data": {
    "tracks": [
      {
        "uri": "spotify:track:rec1",
        "name": "Recommended Song",
        "artist": "Artist Name",
        "album": "Album Name",
        "artworkUrl": "https://...",
        "durationMs": 180000
      }
    ],
    "source": "api",
    "confidence": 0.7
  }
}

Genre-Based Recommendations

GET /api/media/recommendations/genre/rock?provider=spotify&limit=20

Mood-Based Recommendations

GET /api/media/recommendations/mood/happy?provider=spotify&limit=10

**Supported Moods:**

  • happy → Upbeat, energetic
  • sad → Melancholic, emotional
  • focus → Ambient, instrumental
  • relax → Calm, peaceful
  • energetic → High energy, upbeat
  • chill → Laid back, mellow

Get Recommendation History

GET /api/media/recommendations?limit=20

Submit Feedback

POST /api/media/recommendations/feedback
Content-Type: application/json

{
  "recommendationId": "rec-123",
  "score": 0.8,
  "notes": "Great recommendations!",
  "category": "accuracy"
}

**Score Range:** -1.0 (negative) to +1.0 (positive)

Agent Integration

Skill Example

import { PlaybackService } from '@/lib/media/playback-service';
import { PlaylistService } from '@/lib/media/playlist-service';

// Agent skill to play focus music
async function playFocusMusic(tenantId: string) {
  const playbackService = new PlaybackService(tenantId);

  // Play focus playlist
  await playbackService.execute({
    type: 'play',
    value: 'spotify:playlist:37i9dQZF1DX4sWSpwq3LiO', // Focus Flow
    provider: 'spotify',
  });

  return {
    success: true,
    message: 'Playing focus music',
  };
}

// Agent skill to create workout playlist
async function createWorkoutPlaylist(tenantId: string, trackUris: string[]) {
  const playlistService = new PlaylistService(tenantId);

  const playlist = await playlistService.createPlaylist({
    name: 'Workout Mix',
    description: 'High-energy workout songs',
    isPublic: false,
    provider: 'spotify',
    trackUris,
  });

  return {
    success: true,
    playlistId: playlist.id,
    trackCount: trackUris.length,
  };
}

Voice Commands in Agents

// Agent can process natural language commands
async function processMediaCommand(tenantId: string, command: string) {
  const response = await fetch('/api/media/voice', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ command }),
  });

  const result = await response.json();

  if (result.success) {
    return {
      understood: true,
      action: result.data.parsed.action,
      confidence: result.data.parsed.confidence,
    };
  }

  return {
    understood: false,
    suggestion: 'Try saying "play jazz" or "pause the music"',
  };
}

Recommendation Generation

import { RecommendationService } from '@/lib/media/recommendation-service';

// Generate personalized recommendations
async function getDailyMix(tenantId: string) {
  const recommendationService = new RecommendationService(tenantId);

  // Based on listening history
  const result = await recommendationService.generateRecommendations({
    // No seeds = analyze history
  });

  return result.tracks;
}

// Genre-based discovery
async function discoverGenre(tenantId: string, genre: string) {
  const recommendationService = new RecommendationService(tenantId);

  const result = await recommendationService.generateRecommendations({
    genre,
    limit: 20,
  });

  return result.tracks;
}

Database Schema

media_playlists

Stores playlist metadata synced from external providers.

CREATE TABLE media_playlists (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES tenants(id),
  provider VARCHAR(20) NOT NULL, -- 'spotify' | 'apple-music'
  external_id VARCHAR(255) NOT NULL, -- Provider's playlist ID
  name VARCHAR(255) NOT NULL,
  description TEXT,
  is_public BOOLEAN DEFAULT false,
  track_count INTEGER DEFAULT 0,
  artwork_url TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW(),

  UNIQUE(tenant_id, provider, external_id)
);

CREATE INDEX idx_media_playlists_tenant ON media_playlists(tenant_id);
CREATE INDEX idx_media_playlists_provider ON media_playlists(provider);
CREATE INDEX idx_media_playlists_updated ON media_playlists(updated_at DESC);

-- RLS Policy
ALTER TABLE media_playlists ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON media_playlists
  FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::UUID);

media_playlist_tracks

Stores track metadata for playlist contents.

CREATE TABLE media_playlist_tracks (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES tenants(id),
  playlist_id UUID NOT NULL REFERENCES media_playlists(id) ON DELETE CASCADE,
  track_uri TEXT NOT NULL,
  track_name VARCHAR(255),
  artist_names TEXT[],
  album_name VARCHAR(255),
  duration_ms INTEGER,
  artwork_url TEXT,
  position INTEGER,
  added_at TIMESTAMPTZ DEFAULT NOW(),

  UNIQUE(playlist_id, track_uri)
);

CREATE INDEX idx_playlist_tracks_playlist ON media_playlist_tracks(playlist_id);
CREATE INDEX idx_playlist_tracks_tenant ON media_playlist_tracks(tenant_id);

-- RLS Policy
ALTER TABLE media_playlist_tracks ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON media_playlist_tracks
  FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::UUID);

media_recommendations

Stores recommendation history and metadata.

CREATE TABLE media_recommendations (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES tenants(id),
  seed_track_uri TEXT,
  seed_artist_names TEXT[],
  recommended_track_uris TEXT[] NOT NULL,
  provider VARCHAR(20) NOT NULL,
  source VARCHAR(20) NOT NULL, -- 'api' | 'history' | 'genre' | 'mood'
  metadata JSONB,
  created_at TIMESTAMPTZ DEFAULT NOW(),

  valid_for INTEGER DEFAULT 86400 -- Cache validity (seconds)
);

CREATE INDEX idx_recommendations_tenant ON media_recommendations(tenant_id);
CREATE INDEX idx_recommendations_created ON media_recommendations(created_at DESC);

-- RLS Policy
ALTER TABLE media_recommendations ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON media_recommendations
  FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::UUID);

media_recommendation_feedback

Stores user feedback for recommendations.

CREATE TABLE media_recommendation_feedback (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES tenants(id),
  recommendation_id UUID NOT NULL REFERENCES media_recommendations(id) ON DELETE CASCADE,
  score NUMERIC(3, 2) NOT NULL CHECK (score BETWEEN -1 AND 1),
  notes TEXT,
  category VARCHAR(50), -- 'accuracy' | 'helpfulness' | 'diversity'
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_feedback_recommendation ON media_recommendation_feedback(recommendation_id);
CREATE INDEX idx_feedback_tenant ON media_recommendation_feedback(tenant_id);

-- RLS Policy
ALTER TABLE media_recommendation_feedback ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON media_recommendation_feedback
  FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::UUID);

Governance

Agent Maturity Restrictions

Media control actions respect agent maturity levels:

  • **Student Agents**: Read-only access (getState, search, listPlaylists, getPlaylistTracks)
  • **Intern Agents**: Full control with supervisor approval (playback, playlist modifications)
  • **Supervised Agents**: Full control with live monitoring
  • **Autonomous Agents**: Full control without restrictions

Required Actions

Agents need the following actions:

  • MEDIA_CONTROL_PLAYBACK - Play/pause/skip/volume/seek
  • MEDIA_PLAYLIST_MANAGE - Create/modify/delete playlists
  • MEDIA_READ_STATE - Read playback state and device info

Rate Limiting

Media API calls respect tenant rate limits:

  • **Free Tier**: 50 calls/day
  • **Solo Tier**: 500 calls/day
  • **Team Tier**: 5,000 calls/day
  • **Enterprise Tier**: Unlimited

Troubleshooting

OAuth Errors

**Error:** Spotify token exchange failed

  • **Cause:** Invalid authorization code or redirect URI mismatch
  • **Fix:** Verify redirect URI matches Spotify app settings exactly

**Error:** Apple Music configuration missing

  • **Cause:** Missing or invalid environment variables
  • **Fix:** Verify APPLE_MUSIC_KEY_ID, APPLE_MUSIC_TEAM_ID, APPLE_MUSIC_PRIVATE_KEY are set

Token Refresh Failures

**Error:** Spotify token refresh failed

  • **Cause:** Refresh token expired or revoked
  • **Fix:** User must re-authenticate through OAuth flow

**Error:** Developer token generation failed

  • **Cause:** Invalid private key or key ID
  • **Fix:** Verify private key format and key ID match Apple Developer portal

Rate Limiting

**Error:** Spotify API error: 429

  • **Cause:** Too many API requests
  • **Fix:** Implement exponential backoff, retry after Retry-After header

Device Not Found

**Error:** Device not found

  • **Cause:** Device ID invalid or device offline
  • **Fix:** Use getDevices to list available devices, transfer to active device

Playlist Sync Failures

**Error:** Failed to sync playlists from Spotify

  • **Cause:** Network error or API rate limit
  • **Fix:** Retry sync operation, check connection status

Recommendation Issues

**Error:** No seeds provided and no history available

  • **Cause:** New account with no listening history
  • **Fix:** Provide seed tracks or artists explicitly

**Error:** Genre not found

  • **Cause:** Invalid genre name
  • **Fix:** Use common genre names (rock, jazz, pop, hip-hop, electronic)

Testing

Run Media Tests

# Client tests
npm run test -- spotify.unit.test
npm run test -- apple-music.unit.test

# Service tests
npm run test -- playback-service.unit.test
npm run test -- playlist-service.unit.test
npm run test -- recommendation-service.unit.test

# API tests
npm run test -- playback.test.ts
npm run test -- playlists.test.ts
npm run test -- voice.test.ts

Test Coverage

# Generate coverage report
npm run test:coverage

# View HTML report
open coverage/index.html

# Verify coverage thresholds
# - SpotifyClient: >90%
# - AppleMusicClient: >85%
# - Media services: >80%

Mock Configuration

Tests use comprehensive mocks for external dependencies:

// Mock fetch for API calls
global.fetch = vi.fn();

// Mock database
vi.mock('@/lib/database');
vi.mocked(getDatabase).mockReturnValue(mockDb);

// Mock provider clients
vi.mock('@/lib/integrations/spotify');
vi.mock('@/lib/integrations/apple-music');

Best Practices

Provider Auto-Detection

Let the service detect the active provider automatically:

// Good - auto-detect
const state = await playbackService.getState();

// Specify only when needed
const state = await playbackService.getState('spotify');

Error Handling

Always handle provider-specific errors:

try {
  await playbackService.execute({ type: 'play', provider: 'spotify' });
} catch (error) {
  if (error instanceof MediaIntegrationError) {
    console.error('Provider not connected:', error.provider);
  } else if (error instanceof MediaPlaybackError) {
    console.error('Playback failed:', error.action);
  }
}

Feedback Loop

Submit feedback to improve recommendations:

await recommendationService.submitFeedback({
  recommendationId: result.id,
  score: 0.9, // Positive
  notes: 'Great mix of similar artists',
  category: 'accuracy',
});

Cache Invalidation

Genre and mood playlists are cached for 24 hours. Negative feedback invalidates cache:

// Submits -0.5 score, which deletes cached recommendation
await recommendationService.submitFeedback({
  recommendationId: cachedRec.id,
  score: -0.5,
});

Platform Evolution

Phase 63A-01 Status

**Completed:**

  • ✅ OAuth integration for Spotify and Apple Music (Plan 01)
  • ✅ Unified playback service and voice commands (Plan 02)
  • ✅ Playlist management and music recommendations (Plan 03)
  • ✅ Test coverage and documentation (Plan 04) - **Current**

**Next Enhancements:**

  • YouTube Music integration
  • Advanced voice command parsing (NLP/ML)
  • Multi-device synchronization
  • Lyrics fetching and display
  • Social sharing of playlists
  • Real-time collaborative playlists

Support

For issues or questions:

  1. Check troubleshooting section above
  2. Review test files for usage examples
  3. Check Spotify/Apple Music API documentation
  4. Verify environment variables and OAuth configuration

---

*Last Updated: 2026-02-20*

*Version: 1.0.0*